﻿using System;
using Bettzueche.RLGlue.Generics;
using Bettzueche.RLGlue.Codec.Network;
using System.Net;
using System.Reflection;
using System.Diagnostics;
using Bettzueche.RLGlue.RLComponent;

namespace Bettzueche.RLGlue.Codec.Util {

    /// <summary>
    /// This class can be called from the command line to load an agent and create
    /// an executable RL agent program.  
    /// </summary>
    /// <example><code>
    /// // Create an agentLoader that will start the agent when its run method is called
    /// AgentLoader theAgentLoader = AgentLoader.LoadAgent(new SampleSarsaAgent());
    /// // Create an environmentloader that will start the environment when its run method is called
    /// EnvironmentLoader theEnvironmentLoader = EnvironmentLoader.LoadEnvironment(new SampleMinesEnvironment());
    /// // Create threads so that the environment and agent can run asynchronously 	
    /// Thread agentThread=new Thread(theAgentLoader.Run);
    /// Thread environmentThread = new Thread(theEnvironmentLoader.Run);
    /// agentThread.Start();
    /// environmentThread.Start();
    /// // Run the main method of the Sample Experiment
    /// // This will run the experiment in the main thread.
    /// SampleExperiment.Main(args);
    /// </code></example>
    public class AgentLoader {

        String host = Network.Network.kDefaultHost;
        int port = Network.Network.kDefaultPort;
        AnAgent theAgent = null;
        IClientAgent theClient = null;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="theAgent">Implementierung von IAgent&lt;A,O&gt;</param>
        /// <remarks>
        /// Falls die Benutzer-Umgebungsvariablen "RLGLUE_HOST" und "RLGLUE_PORT" gesetzt sind, werden diese genutzt,
        /// um eine Verbindung zum RLGlue Host aufzubauen.
        /// </remarks>
        protected AgentLoader(AnAgent theAgent) {
            this.theAgent = theAgent;
            String envVariableHostString = Environment.GetEnvironmentVariable("RLGLUE_HOST",EnvironmentVariableTarget.User);
            String envVariablePortString = Environment.GetEnvironmentVariable("RLGLUE_PORT",EnvironmentVariableTarget.User);
            setHostAndPorts(envVariableHostString, envVariablePortString);
        }

        protected AgentLoader(String hostString, String portString, AnAgent theAgent) : this(theAgent) {
            setHostAndPorts(hostString, portString);
        }


        /// <summary>
        /// Setzt host und port. Bei Fehler oder Null bleiben Defaultwerte unverändert.
        /// </summary>
        /// <param name="hostString">ip oder dns-name des Hosts. Wenn null, wird Default-Wert genommen</param>
        /// <param name="portString">Port des Hosts. Wenn null, wird Default-Wert genommen</param>
        /// <remarks>
        /// Description of Brian Tanner's Java Version:
        /// Try these new settings.  We'll only actually set them if they seem valid.
        /// </remarks>
        private void setHostAndPorts(String hostString, String portString) {

            //Now override the default or env variable port and string with these specific settings
            if (hostString != null) {
                Dns.GetHostAddresses(hostString);
                try {
                    IPAddress[] ips = Dns.GetHostAddresses(hostString);
                    if (ips.Length > 0)
                        host = hostString;
                    else
                        System.Diagnostics.Trace.TraceWarning("Unknown host {0}. Using default localhost instead.");
                } catch (Exception e) {
                    System.Diagnostics.Trace.TraceError("Error resolving hoststring {0}: {1}",hostString,e.Message);
                }
            }

            if (portString != null) {

                try {
                    int parsedPort = Int32.Parse(portString);

                    if (parsedPort < 0 || parsedPort > 65535) {
                        System.Diagnostics.Trace.TraceWarning("Could not use port you requested: {0} is not a valid port number. Using default port instead.");
                    }
                    else {
                        port = parsedPort;
                    }
                } catch (Exception e) {
                    System.Diagnostics.Trace.TraceError("Error resolving portstring {0}: {1}",portString,e.Message);
                }
            }
        }


        /// <summary>
        /// Gibt eine Instanz AgentLoader.<para>
        /// Lädt vom angegebenen Assembly die IAgent-Implementierung.</para>
        /// </summary>
        /// <param name="assemblyPath">full qualified path to the assembly, implementing
        ///     <see cref="Bettzueche.RLGlue.RLComponent.IAgent">IAgent</see> or
        ///     <see cref="Bettzueche.RLGlue.Generics.IGenericAgent{A,O}">IGenericAgent&lt;A,O&gt;</see>.</param>
        /// <param name="fullClassName">full qualified name of the Agent implementation class.</param>
        /// <param name="hostString"></param>
        /// <param name="portString"></param>
        /// <returns></returns>
        /// <remarks>
        /// Beschreibung der entsprechenden Java-Methode von Brian Tanner:<para>
        /// Loads the class agentClassName as an rl-glue agent.
        /// Doesn't make much sense to do the work in here... we can do it in a constructor
        /// and then get things straight about who is supposed to be setting things from 
        /// the environment variables.
        /// </para>
        /// </remarks>
        public static AgentLoader LoadAgent(String assemblyPath, String fullClassName, String hostString, String portString) {
            IAgent agent = null;
            AgentLoader theLoader = null;
            try {
                //Have to use the system classloader because if the codec is installed,
                //class.forName will use the ext classloader which won't find your classes.
                Assembly agentAssembly = Assembly.LoadFrom(assemblyPath);
                Type agentType = agentAssembly.GetType(fullClassName,true);
                Type iAgentType = agentType.GetInterface("Bettzueche.RLGlue.RLComponent.IAgent");

                agent = (IAgent)Activator.CreateInstance(agentType); // Instanz des geladenene Agents (muss Default-Konstruktor haben)

                if (hostString == null || portString == null)
                    theLoader = new AgentLoader(agent);
                else
                    theLoader = new AgentLoader(hostString, portString, agent);
                theLoader.theClient = new ClientAgent(agent);
            } catch (Exception ex) {
                Trace.TraceError("LoadAgent({0}) threw Exception: {1}",assemblyPath, ex);
                Console.WriteLine(ex.StackTrace);
                Environment.Exit(1);
            }
            return theLoader;
        }

        /// <summary>
        /// Loads the agent.
        /// </summary>
        /// <param name="agent">The agent.</param>
        /// <returns>Instance of <see cref="AgentLoader"/></returns>
        public static AgentLoader LoadAgent(IAgent agent) {
            AgentLoader retVal = new AgentLoader(agent);
            retVal.theClient = new ClientAgent(agent);
            return retVal;
        }

        /// <summary>
        /// Loads the agent.
        /// </summary>
        /// <typeparam name="A">Actions type</typeparam>
        /// <typeparam name="O">Observations type</typeparam>
        /// <param name="agent">The agent.</param>
        /// <returns>Instance of <see cref="AgentLoader"/></returns>
        internal static AgentLoader LoadAgent<A, O>(IGenericAgent<A, O> agent)
            where A : struct
            where O : struct
        {
            AgentLoader retVal = new AgentLoader(agent);
            retVal.theClient = new GenericClientAgent<A, O>(agent);
            return retVal;
        }

        /// <summary>
        /// Gibt eine Instanz AgentLoader.<para>
        /// Lädt vom angegebenen Assembly die IAgent-Implementierung.</para>
        /// </summary>
        /// <param name="agentClassName">Vollqualifizierter Ihres Assemblies, das die Implementierung von
        /// <see cref="Bettzueche.RLGlue.AnAgent">IAgent&lt;A,O&gt;</see> enthält.</param>
        /// <param name="hostString"></param>
        /// <param name="portString"></param>
        /// <returns></returns>
        /// <remarks>
        /// Beschreibung der entsprechenden Java-Methode von Brian Tanner:<para>
        /// Loads the class agentClassName as an rl-glue agent.
        /// Doesn't make much sense to do the work in here... we can do it in a constructor
        /// and then get things straight about who is supposed to be setting things from 
        /// the environment variables.
        /// </para>
        /// </remarks>
        public static AgentLoader LoadGenericAgent(String assemblyPath, String fullClassName, String hostString, String portString) {
            AnAgent agent = null;
            AgentLoader theLoader = null;
            try {
                //Have to use the system classloader because if the codec is installed,
                //class.forName will use the ext classloader which won't find your classes.
                Assembly agentAssembly = Assembly.LoadFrom(assemblyPath);
                Type agentType = agentAssembly.GetType(fullClassName,true);
                Type iAgentType = agentType.GetInterface("Bettzueche.RLGlue.Generics.IGenericAgent`2");
                Type[] genArgs = iAgentType.GetGenericArguments(); //liefert typeof A und O
                Type clAgentType = typeof(GenericClientAgent<,>); //allg. gener. Typ ClientAgent<?,?>
                clAgentType = clAgentType.MakeGenericType(genArgs); //konkr. gener. Typ ClientAgent<A,O>
                agent = (AnAgent)Activator.CreateInstance(iAgentType); // Instanz des geladenene Agents (muss Default-Konstruktor haben)

                if (hostString == null || portString == null)
                    theLoader = new AgentLoader(agent);
                else
                    theLoader = new AgentLoader(hostString, portString, agent);
                theLoader.theClient = (IClientAgent)Activator.CreateInstance(clAgentType);
            } catch (Exception ex) {
                Trace.TraceError("loadAgent({0}) threw Exception: {1}",assemblyPath, ex);
                Console.WriteLine(ex.StackTrace);
                Environment.Exit(1);
            }
            return theLoader;
        }

        public void KillProcess() {
            theClient.KillProcess();
        }

        /// <summary>
        ///  Startet mittels eines <see cref="ClientAgent">ClientAgents</see> das Experiment.
        /// </summary>
        public void Run() {
            String ImplementationVersion = RLGlueCore.GetImplementationVersion();
            String SpecVersion = RLGlueCore.GetSpecVersion();

            Console.WriteLine("RL-Glue .NET Agent Codec Version: " + SpecVersion + " (" + ImplementationVersion + ")");
            Console.WriteLine("\tConnecting to " + host + " on port " + port + "...");

            try {
                theClient.Connect(host, port, Network.Network.kRetryTimeout);
                Console.WriteLine("\tAgent Codec Connected");
                theClient.RunAgentEventLoop();
                theClient.Close();
            } catch (Exception e) {
                Trace.TraceError("AgentLoader run({0}) threw Exception: {1}" + e, theAgent.GetType().FullName, e.StackTrace);
                Console.WriteLine(e.StackTrace);
            }
        }

        
        /// <summary>
        /// Einstiegspunkt für AgentLoader.exe<para>
        /// Lädt vom angegebenen Assembly (erster Parameter) eine Agent-Implementierung und startet den Agent für ein Experiment.</para>
        /// </summary>
        /// <param name="args"></param>
        /// <returns>1 bei Fehler, 0 sonst</returns>
        /// <remarks>
        /// Aufruf von AgentLoader.exe PathToMyAssembly :
        ///  Falls Benutzer-Umgebungsvariablen "RLGLUE_HOST" und "RLGLUE_PORT" gesetzt sind, werden diese benutzt,
        ///  ansonsten die Default-Werte.
        /// Aufruf von AgentLoader.exe PathToMyAssembly MyHost MyPort:
        ///  Verbindung zu RLGLue am angegebenen Host und Port
        /// <para>
        /// Das Assembly muss eine Implementierung von IAgent&lt;A,O&gt; enthalten.
        /// </para>
        /// </remarks>
        static void Main(string[] args) {
            String usage = "AgentLoader.exe <PathToAgentAssembly> <FullAgentClassName> [<RLGlueHost> <RLGLuePort>] [-generic] ";

            String envVars = "If you call AgentLoader.exe without Host and Port, the following environment variables are used by the agent to control its function:" + Environment.NewLine +
                    "RLGLUE_HOST  : If set the agent will use this ip or hostname to connect to rather than " + Network.Network.kDefaultHost + Environment.NewLine +
                    "RLGLUE_PORT  : If set the agent will use this port to connect on rather than " + Network.Network.kDefaultPort + Environment.NewLine;
            AgentLoader theLoader = null;
            bool useGeneric = false;
            string host = null;
            string port = null;
            if (args.Length >= 5)
                useGeneric = args[5].Equals("-generic", StringComparison.InvariantCultureIgnoreCase);
            if (args.Length >= 4) {
                host = args[3];
                port = args[4];
            }
            if (args.Length >= 2) {
                if (useGeneric)
                    theLoader = LoadGenericAgent(args[0], args[1], host, port);
                else
                    theLoader = LoadAgent(args[0], args[1], host, port);
            }
            else {
                Console.WriteLine(usage);
                Console.WriteLine(envVars);
                Console.WriteLine("\n\t-generic : Load a generic implementaion of IGenericAgent");
                Environment.Exit(1);
            }
            theLoader.Run(); //<- Das ist auch in Java kein Thread Start
        }



    }


}
